跳到主要内容

Spring Bean 的杂项知识

Bean 的作用域有哪些?​

ScopeDescription
singleton单例模式 (默认就是单例模式)
prototype原型模式 每次从容器中 get 的时候,都会产生一个新对象
request将单个 Bean 定义限定为单个 HTTP 请求的生命周期。
session将单个 Bean 定义限定为 HTTP 会话的生命周期。
application将单个Bean 定义限定为 ServletContext 的生命周期。
websocket将单个 Bean 定义限定为 WebSocket 的生命周期。

request、session、application、websocket 仅在支持 web 的 Spring 应用程序上下文中有效。

<bean id="student2" class="com.alsritter.pojo.Student" p:name="张三" scope="prototype"/>

因为使用了原型模式,所以每取得一次就创建一个

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) context.getBean("student2");
Student student2 = (Student) context.getBean("student2");
System.out.println(student == student2);
}
-->
false

Bean 单例的线程安全​

当多个线程操作同一个对象的时候,对这个对象的成员变量的写操作会存在线程安全问题,而容器本身并没有提供 Bean 的线程安全策略,因此可以说 Spring 容器中的 Bean 本身不具备线程安全的特性,但是具体还是要结合具体 scope 的 Bean 去研究

先从单例与原型 Bean 分别进行说明

原型 Bean​

对于原型 Bean,每次创建一个新对象,也就是线程之间并不存在 Bean 共享,自然是不会有线程安全的问题。

单例 Bean​

对于单例 Bean,所有线程都共享一个单例实例 Bean,因此是存在资源的竞争。如果单例 Bean,是一个无状态 Bean,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的。比如 Spring mvc 的 Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。

下面是一个有状态的 Bean 示例:(经典的并发问题)

public class TestManagerImpl implements TestManager {  
private User user;
public void deleteUser(User e) throws Exception {
user = e; //1
prepareData(e);
}

public void prepareData(User e) throws Exception {
user = getUserByID(e.getId()); //2
//使用user.getId(); //3
}

}

如果该 Bean 配置为 singleton,会出现什么样的状况呢? 如果有2个用户访问,都调用到了该 Bean,假定为 user1、user2。

当 user1 调用到程序中的步骤1 的时候,该 Bean 的私有变量 user 被赋值为 user1,当 user1 的程序走到步骤2 的时候,该 Bean 的私有变量 user 被重新赋值为 user1_create,理想的状况,当 user1 走到 3步骤的时候,私有变量 user 应该为user1_create ;但如果在 user1 调用到 3步骤之前,user2 开始运行到了步骤1了,由于单态的资源共享,则私有变量 user 被修改为 user2;这种情况下,user1 的步骤3 用到的 user.getId() 实际用到是 user2 的对象。

如果需要在 Bean 里保存状态常见的有 2 种解决办法:

  • 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
  • 改变 Bean 的作用域为 “prototype”:每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。

@Component 和 @Bean 的区别​

作用对象不同: @Component 注解作用于类,而 @Bean 注解作用于方法。

Spring 帮助我们管理 Bean 分为两个部分,一个是注册 Bean,一个装配 Bean。

完成这两个动作有三种方式,一种是使用自动配置的方式、一种是使用 JavaConfig 的方式,一种就是使用 XML 配置的方式。(注意,虽然后面开始使用注解了,但是早在 XML 时期就这么分了)

@Compent 作用就相当于 XML配置,@Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)

@Component
public class Student {

private String name = "lkm";

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

@Bean 需要在配置类中使用,即类上需要加上 @Configuration 注解

@Configuration
public class WebSocketConfig {
@Bean
public Student student(){
return new Student();
}
}

相当于

<beans>
<bean id="student" class="com.temp.Student"/>
</beans>

两者都可以通过 @Autowired 装配

@Autowired
Student student;

那为什么有了 @Compent,还需要 @Bean 呢?

1、如果想要将 第三方库中的组件 装配到应用中,在这种情况下,是没有办法在它的类上添加 @Component 注解的,因此就不能使用自动化装配的方案了,但是这时可以使用 @Bean,当然也可以使用 XML 配置。

2、使用 @Bean 进行装配更加的灵活,如下代码

// 这个 status 会根据类型自动注入
@Bean
public OneService getService(status) {
switch (status) {
case 1:
return new serviceImpl1();
case 2:
return new serviceImpl2();
case 3:
return new serviceImpl3();
}
}

注册为 Bean 的注解​

一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean不知道属于哪个层,可以使用 @Component 注解标注。
  • @Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
  • @Controller: 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。

Bean 的一生​

Spring Bean 的生命周期只有下面这四个阶段

  • 实例化 Instantiation:包装对象,执行构造方法,用于提前暴露实例解决循环依赖问题
  • 属性赋值 Populate:寻找并且注入依赖,依赖的 Bean 还会递归调用 getBean 方法获取)
  • 初始化 Initialization:调用自定义的初始化方法(不是构造方法,而是调用 <bean> 的 init-method 属性指定的初始化方法))
  • 销毁 Destruction

实例化 -> 属性赋值 -> 初始化 -> 销毁

实例化和属性赋值对应构造方法和 setter 方法的注入,初始化和销毁是用户能自定义扩展的两个阶段。

Bean 的创建主要逻辑都在 doCreate() 方法中,逻辑很清晰,就是顺序调用以下三个方法,这三个方法与三个生命周期阶段一一对应

  • createBeanInstance() -> 实例化
  • populateBean() -> 属性赋值
  • initializeBean() -> 初始化

这个 doCreateBean 位于 org.springframework.beans.factory.support 包下

// 忽略了无关代码
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
// 实例化阶段!
instanceWrapper = createBeanInstance(beanName, mbd, args);
}

final Object bean = instanceWrapper.getWrappedInstance();

// Initialize the bean instance.
Object exposedObject = bean;
try {
// 属性赋值阶段!
populateBean(beanName, mbd, instanceWrapper);
// 初始化阶段!
exposedObject = initializeBean(beanName, exposedObject, mbd);
}

}

至于销毁,是在容器关闭时调用的,详见 ConfigurableApplicationContext#close()

注意:使用 final 修饰形参分两种情况,修饰基本类型时是为了让这个参数只读,修饰引用类型时是为了防止引用的地址被改变,否则会无法通过编译

Reference​

参考资料 请别再问Spring Bean的生命周期了! 参考资料 javaguide Spring bean 参考资料 5.3 @Component 和 @Bean 的区别是什么? 参考资料 @Component 和 @Bean 的区别 参考资料 Spring 中的bean 是线程安全的吗?